import wx
from types import UnicodeType, StringType, LongType, IntType
from wx.lib.masked import Field, BaseMaskedTextCtrl

class DHMTimeCtrl(BaseMaskedTextCtrl):
    """
    HMTimeCtrl displays Days:Hours:Minutes. Hours is not limited to any value.
    """
    def __init__(self, parent, id = -1, value = '00D:00H:00M',
                 pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.TE_PROCESS_TAB):
        
        # Initialize the base control
        BaseMaskedTextCtrl.__init__(self, parent, id = id, pos = pos, size = size,
                                    style = style, validator = wx.DefaultValidator,
                                    name = "HMTimeCtrl", setupEventHandling = False)

        # ToolTip
        self.SetToolTipString(_("Days:Hours:Minutes"))
        
        # Create the spin button
        self.__spinButton = wx.SpinButton(parent, -1, wx.DefaultPosition, (-1, self.GetSize()[1]), wx.SP_VERTICAL)

        # current position
        self.__posCurrent = 0
        
        # hour field format
        daysfield = Field(formatcodes='0r<SV', validRegex='\d\d', mask = "##D", validRequired=True)
        hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', mask = "##H", validRequired=True)
        minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', mask = "##M", validRequired=True)
        fields = [daysfield, hourfield, minutefield]

        params = {}
        params['mask'] = "##D:##H:##M"
        params['formatcodes'] = 'F'
        params['fields'] = fields
        params['retainFieldValidation'] = True
        self.SetCtrlParameters(**params)
        
        self.SetMaskParameters(validRequired = False)

        # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
        self._SetKeyHandler(':', self._OnChangeField)


        # This makes the up/down keys act like spin button controls:
        self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp)
        self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown)

        # Events Table
        self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton)
        self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton)
        self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
        self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
        self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection)    ## limit selections to single field
        self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick
        self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab.
        self.Bind(wx.EVT_CHAR, self.__OnChar )              ## remove "shift" attribute from colon key event,
                                                            ## then call BaseMaskedTextCtrl._OnChar with
                                                            ## the possibly modified event.
        # Set initial value
        self.SetValue(value)
            
    def SetValue(self, value):
        if type(value) in (UnicodeType, StringType):
            self._SetValue(value)
        elif type(value) in (LongType, IntType):
            days = value / 86400
            hour = value / 3600 - (days * 24)
            minute = value / 60 - ((days * 24 * 60) + (hour * 60))
            strvalue = "%02dD:%02dH:%02dM" %(days, hour, minute)
            self._SetValue(strvalue)
        else:
            raise ValueError, "Illegal type passed to HMTimeCtrl::SetValue. int or string required"
                
    def GetValue(self, asInt = False):
        value = BaseMaskedTextCtrl.GetValue(self)
        value.strip()
        if asInt:
            days, hours, minutes = value.split(":")
            days = days[:2]
            hours = hours[:2]
            minutes = minutes[:2]
            value = int(days)*86400 + int(hours)*3600 + int(minutes)*60
        return value

    def GetSpinButton(self):
        return self.__spinButton
    
    def __repr__(self):
        return "<HMTimeCtrl: %s>" % self.GetValue()

#-------------------------------------------------------------------------------------------------------------
# these are private functions and overrides:
        
    def SetInsertionPoint(self, pos):
        BaseMaskedTextCtrl.SetInsertionPoint(self, pos)
        self.__posCurrent = self.GetInsertionPoint()

    def SetSelection(self, sel_start, sel_to):
        # Adjust selection range to legal extent if not already
        if sel_start < 0:
            sel_start = 0

        if self.__posCurrent != sel_start:                      # force selection and insertion point to match
            self.SetInsertionPoint(sel_start)
        cell_start, cell_end = self._FindField(sel_start)._extent
        if not cell_start <= sel_to <= cell_end:
            sel_to = cell_end

        self.__bSelection = sel_start != sel_to
        BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
        
    def __OnSpin(self, key):
        """
        This is the function that gets called in response to up/down arrow or
        bound spin button events.
        """
        self.__IncrementValue(key, self.__posCurrent)   # changes the value

        # Ensure adjusted control regains focus and has adjusted portion
        # selected:
        self.SetFocus()
        start, end = self._FindField(self.__posCurrent)._extent
        self.SetInsertionPoint(start)
        self.SetSelection(start, end)

    def __OnSpinUp(self, event):
        """
        Event handler for any bound spin button on EVT_SPIN_UP;
        causes control to behave as if up arrow was pressed.
        """
        self.__OnSpin(wx.WXK_UP)
        keep_processing = False
        return keep_processing


    def __OnSpinDown(self, event):
        """
        Event handler for any bound spin button on EVT_SPIN_DOWN;
        causes control to behave as if down arrow was pressed.
        """
        self.__OnSpin(wx.WXK_DOWN)
        keep_processing = False
        return keep_processing
    
    def __OnChar(self, event):
        """
        Handler to explicitly look for ':' keyevents, and if found,
        clear the m_shiftDown field, so it will behave as forward tab.
        It then calls the base control's _OnChar routine with the modified
        event instance.
        """
        keycode = event.GetKeyCode()
        if keycode == ord(':'):
            event.m_shiftDown = False
        BaseMaskedTextCtrl._OnChar(self, event)              ## handle each keypress
        
    def __LimitSelection(self, event):
        """
        Event handler for motion events; this handler
        changes limits the selection to the new cell boundaries.
        """
        pos = self.GetInsertionPoint()
        self.__posCurrent = pos
        sel_start, sel_to = self.GetSelection()
        selection = sel_start != sel_to
        if selection:
            # only allow selection to end of current cell:
            start, end = self._FindField(sel_start)._extent
            if sel_to < pos:   sel_to = start
            elif sel_to > pos: sel_to = end

        self.SetInsertionPoint(self.__posCurrent)
        self.SetSelection(self.__posCurrent, sel_to)
        if event: event.Skip()
        
    def __IncrementValue(self, key, pos):
        text = self.GetValue()
        field = self._FindField(pos)
        start, end = field._extent
        slice = text[start:end]
        if key == wx.WXK_UP:
            increment = 1
        else:
            increment = -1

        if field._index == 0:   # days;
            newslice = "%02d" % ((int(slice) + increment) % 100)
        if field._index == 1:   # hours;
            newslice = "%02d" % ((int(slice) + increment) % 24)
        if field._index == 2:   # minutes;
            newslice = "%02d" % ((int(slice) + increment) % 60)
            
        newvalue = text[:start] + newslice + text[end:]
        self.SetValue(newvalue)
        
if __name__ == '__main__':
    class TestPanel(wx.Panel):
        def __init__(self, parent, id, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.TAB_TRAVERSAL):
            wx.Panel.__init__(self, parent, id, pos, size, style)
            
            self.tc = HMTimeCtrl(self, -1)

            sizer = wx.BoxSizer( wx.HORIZONTAL )
            sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 )
            sizer.Add( self.tc.GetSpinButton(), 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 )

            self.SetAutoLayout( True )
            self.SetSizer( sizer )
            sizer.Fit( self )
            sizer.SetSizeHints( self )

    class MyApp(wx.App):
        def OnInit(self):
            frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) )
            panel = TestPanel(frame, -1)
            frame.Show(True)
            return True
    _ = lambda x:x
    app = MyApp(0)
    app.MainLoop()
